﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ServiceModel;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using QUT.Bio.SilverMap.DefaultProvider;
using QUT.Bio.SilverMap.MapService;
using QUT.Bio.SilverMap.ObjectModel;
using QUT.Bio.Util;
using QUT.Bio.Graphs;

namespace QUT.Bio.SilverMap.DefaultProvider {
	/// <summary>
	/// DataLayer is responsible for interaction with the web service that supplies precomputed BLAST results.
	/// </summary>

	internal class DataLayer {

		private ObjectModel model;
		private PopupOverlay overlay;
		private string webServiceAddress;

		/// <summary>
		/// Initialise the data layer.
		/// </summary>
		/// <param name="model">The data structure that will hold BLAST results.</param>
		/// <param name="overlay">A PopupOverlay that is used to display </param>
		/// <param name="webServiceAddress"></param>

		public DataLayer (
			ObjectModel model,
			PopupOverlay overlay,
			string webServiceAddress
		) {
			this.model = model;
			this.overlay = overlay;
			this.webServiceAddress = webServiceAddress.Replace("localhost", GetDomain() );
		}

		private string GetDomain () {
			#if SILVERLIGHT
				string [] parts = System.Windows.Browser.HtmlPage.Document.DocumentUri.AbsoluteUri.Split( new char [] { '/', ':' }, StringSplitOptions.RemoveEmptyEntries );
				return parts[1];
			#else
				return "http://bio.mquter.qut.edu.au/SilverMap";
			#endif
		}

		/// <summary>
		/// Initialise 
		/// </summary>
		/// <param name="model"></param>
		/// <param name="overlay"></param>

		public DataLayer ( ObjectModel model, PopupOverlay overlay )
			: this( model, overlay, Configuration.Args["MapWebServiceAddress"] ) {
		}

		public string WebServiceAddress {
			get {
				return webServiceAddress;
			}
		}

		private MapServiceSoapClient GetSoapClient () {
			BasicHttpBinding binding = new BasicHttpBinding {
				MaxReceivedMessageSize = int.MaxValue,
				MaxBufferSize = int.MaxValue
			};

#if ! SILVERLIGHT
			binding.ReaderQuotas.MaxArrayLength = int.MaxValue;
			binding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
			binding.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
#endif

			MapServiceSoapClient res = null;

			try {
				res = new MapServiceSoapClient(
					binding,
					new EndpointAddress( webServiceAddress )
				);
				res.InnerChannel.OperationTimeout = new TimeSpan( 0, 1, 0 );
			}
#pragma warning disable 168
			catch ( UriFormatException uriFormatException ) {
				Alert.Show( "The address supplied for the SilverMap web service ({0}) is not valid.", webServiceAddress );
			}
#pragma warning restore 168
			catch ( Exception e ) {
				Alert.Show( "Error connecting to map web service:\n{0}", e );
			}

			return res;
		}

		private class ParseError : Exception {
			public ParseError ( string format, params object[] args )
				: base( string.Format( format, args ) ) {
			}
		}

		public static string[] ListAvailableDistanceTypes () {
			List<string> result = new List<string>();
			Type t = typeof( DistanceType );

			for ( int i = 0; Enum.IsDefined( t, i ); i++ ) {
				result.Add( Enum.GetName( t, i ) );
			}

			return result.ToArray();
		}

		public void ListAvailableChromosomes ( Action<Chromosome[]> process ) {
			// Status.WriteLine( "Load started at {0}", DateTime.Now );
			MapServiceSoapClient s = GetSoapClient();

			if ( s == null ) {
				return;
			}

			s.ListAvailableOrganismsCompleted += delegate( object sender, ListAvailableOrganismsCompletedEventArgs args ) {
				if ( AllOk( "ListAvailableChromosomes", args ) ) {
					// Status.WriteLine( "Chromosome.Load - {0} records loaded", args.Result.Length );

					#if SILVERLIGHT
					Chromosome[] result = new Chromosome[args.Result.Count];
					#else
					Chromosome[] result = new Chromosome[args.Result.Length];
					#endif

                    int i = 0;

					foreach ( var org in args.Result ) {
						result[i++] = new Chromosome( org );
					}

					process( result );
				}
			};

			s.ListAvailableOrganismsAsync( process );
		}

		public void ListGenes (
			string accession,
			string query,
			int maxGenes,
			Action<Gene> processGene,
			Action loadComplete
		) {
			// Status.WriteLine( "Gene.ListGenes: started at {0}", DateTime.Now );
			MapServiceSoapClient sc = GetSoapClient();

			if ( sc == null ) {
				return;
			}

			PopupWindow loading = overlay.CreateInfoWindow( "Searching for genes...", 300, 120 );

			sc.ListGenesCompleted += delegate( object sender, ListGenesCompletedEventArgs args ) {
				loading.Container = null;

				#region ListGenes completed
				// Status.WriteLine( "Gene.ListGenes: completed at {0}", DateTime.Now );

				if ( AllOk( "Gene.ListGenes", args ) ) {
					StringReader r = new StringReader( args.Result );

					for ( ; ; ) {
						string s = r.ReadLine();

						if ( s == null ) {
							break;
						}

						ParseGene( s, processGene );
					}

					loadComplete();
				}
				#endregion
			};

			sc.ListGenesAsync( accession, query, maxGenes );
		}

		public void ListGenes (
			IEnumerable<string> accessions,
			string query,
			int maxGenes,
			Action<Gene> processGene,
			Action loadComplete
		) {
			// Status.WriteLine( "Gene.ListGenes: started at {0}", DateTime.Now );
			MapServiceSoapClient sc = GetSoapClient();

			if ( sc == null ) {
				return;
			}

			PopupWindow loading = overlay.CreateInfoWindow( "Searching for genes...", 300, 120 );

			sc.ListGenesFromMultipleSequencesCompleted += delegate( object sender, ListGenesFromMultipleSequencesCompletedEventArgs args ) {
				loading.Container = null;

				#region ListGenes completed
				// Status.WriteLine( "Gene.ListGenes: completed at {0}", DateTime.Now );

				if ( AllOk( "Gene.ListGenes", args ) ) {
					StringReader r = new StringReader( args.Result );

					for ( ; ; ) {
						string s = r.ReadLine();

						if ( s == null ) {
							break;
						}

						ParseGene( s, processGene );
					}

					// Status.WriteLine( "Gene.ListGenes: {0} genes loaded.", genes.Count );

					loadComplete();
				}
				#endregion
			};

			sc.ListGenesFromMultipleSequencesAsync( accessions.Join( "," ), query, maxGenes );
		}

		public void GetBasicGeneInfo (
			IEnumerable<decimal> queryIds,
			Action<Gene> processGene,
			Action loadComplete
		) {
			string methodName = "DataLayer.GetBasicGeneInfo";
			// Status.WriteLine( "{0}: started at {1}", methodName, DateTime.Now );

			MapServiceSoapClient sc = GetSoapClient();

			if ( sc == null ) {
				return;
			}

			PopupWindow loading = overlay.CreateInfoWindow( "Getting gene info...", 300, 120 );

			sc.GetBasicGeneInfoCompleted += delegate( object sender, GetBasicGeneInfoCompletedEventArgs args ) {
				loading.Container = null;

				#region GetBasicGeneInfo completed
				// Status.WriteLine( "{0}: finished at {1}", methodName, DateTime.Now );

				if ( AllOk( methodName, args ) ) {
					StringReader r = new StringReader( args.Result );

					try {
						ParseGenes( r, processGene );
					}
					catch ( ParseError e ) {
						Alert.Show( "Parse error in GetBasicGeneInfo:\n{0}", e.Message );
						return;
					}

					loadComplete();
				}
				#endregion
			};

			sc.GetBasicGeneInfoAsync( queryIds.Join( "," ) );
		}

		public void GetSpecificGeneInfo (
			IEnumerable<decimal> queryIds,
			string[] requestedFeatures,
			Action<Dictionary<string, string>[]> processResults
		) {
			string methodName = "DataLayer.GetSpecificGeneInfo";
			// Status.WriteLine( "{0}: started at {1}", methodName, DateTime.Now );

			MapServiceSoapClient sc = GetSoapClient();

			if ( sc == null ) {
				return;
			}

			sc.GetSpecificGeneInfoCompleted += delegate( object sender, GetSpecificGeneInfoCompletedEventArgs args ) {
				// Status.WriteLine( "{0}: finished at {1}", methodName, DateTime.Now );

				if ( AllOk( methodName, args ) ) {
					List<Dictionary<string, string>> results = new List<Dictionary<string, string>>();

					StringReader r = new StringReader( args.Result );

					try {
						ParseRecords( results, r );
					}
					catch ( ParseError e ) {
						Alert.Show( "Parse error in GetSpecificGeneInfo:\n{0}", e.Message );
						return;
					}

					processResults( results.ToArray() );
				}
			};

			sc.GetSpecificGeneInfoAsync( queryIds.Join( "," ), requestedFeatures.Join( "," ) );
		}

		public void AlignOnDemand (
			decimal queryId,
			decimal hitId,
			Action<string, string, string> process
		) {
			string methodName = "DataLayer.AlignOnDemand";
			// Status.WriteLine( "{0}: started at {1}", methodName, DateTime.Now );

			MapServiceSoapClient sc = GetSoapClient();

			if ( sc == null ) {
				return;
			}

			sc.AlignOnDemandCompleted += delegate( object sender, AlignOnDemandCompletedEventArgs args ) {
				// Status.WriteLine( "{0}: finished at {1}", methodName, DateTime.Now );

				if ( AllOk( methodName, args ) ) {
					process( args.Result.Query, args.Result.MidLine, args.Result.Hit );
				}
			};

			sc.AlignOnDemandAsync( queryId, hitId );
		}

		public void GetBlastResults (
			decimal queryId,
			DistanceType distanceType,
			double cutoff,
			string[] accessions,
			int maxHits,
			Action<Gene> processGene,
			Action<Hit> processHit,
			Action loadComplete
		) {
			string methodName = "DataLayer.GetItemsRelatedTo";
			// Status.WriteLine( "{0}: started at {1}", methodName, DateTime.Now );

			MapServiceSoapClient sc = GetSoapClient();

			if ( sc == null ) {
				return;
			}

			PopupWindow progress = overlay.CreateInfoWindow( "Loading hits", 320, 120 );

			sc.GetBlastResultsCompleted += delegate( object sender, GetBlastResultsCompletedEventArgs args ) {
				progress.Container = null;

				// Status.WriteLine( "{0}: finished at {1}", methodName, DateTime.Now );

				if ( AllOk( methodName, args ) ) {
					List<Gene> genes = new List<Gene>();
					List<Hit> hits = new List<Hit>();

					StringReader r = new StringReader( args.Result );

					try {
						ParseGenes( r, processGene );
						ParseHits( r, processHit );
						loadComplete();
					}
					catch ( ParseError e ) {
						Alert.Show( "Parse error in blast results:\n{0}", e.Message );
						return;
					}
				}
			};

			sc.GetBlastResultsAsync( queryId, distanceType, cutoff, string.Join( ",", accessions ), maxHits );
		}

		private string ParseGenes (
			StringReader r,
			Action<Gene> geneAction
		) {
			string s = r.ReadLine();

			if ( s != "Begin Genes" ) {
				throw new ParseError( "Error parsing genes - expected 'Begin Genes' but encountered '{0}'", s );
			}

			for ( ; ; ) {
				s = r.ReadLine();

				if ( s == null ) {
					throw new ParseError( "Unexpected end of input while parsing genes." );
				}

				if ( s == "End Genes" ) {
					break;
				}

				ParseGene( s, geneAction );
			}
			return s;
		}

		private void ParseGene ( string s, Action<Gene> processGene ) {
			string[] t = s.Split( '\t' );

			try {
				string translation = t[7];
				string note = Unescape( t[8] );

				Gene g = new Gene(
					decimal.Parse( t[0] ),
					model.Chromosomes[Unescape( t[1] )],
					Unescape( t[2] ),
					Unescape( t[3] ),
					Unescape( t[4] ),
					translation.Length
				) {
					Translation = translation,
					Note = note
				};

				g.xref.Add( new Xref( "GI", Unescape( t[5] ) ) );
				g.xref.Add( new Xref( "COG", Unescape( t[6] ) ) );

				processGene( g );
			}
			catch ( Exception e ) {
				throw new ParseError( "Error loading genes:\n{0}", e );
			}
		}

		private string ParseHits ( StringReader r, Action<Hit> processHit ) {
			string s = r.ReadLine();

			if ( s != "Begin Hits" ) {
				throw new ParseError( "Error parsing genes - expected 'Begin Hits' but encountered '{0}'", s );
			}

			for ( ; ; ) {
				s = r.ReadLine();

				if ( s == null ) {
					throw new ParseError( "Unexpected end of input while parsing hits." );
				}

				if ( s == "End Hits" ) {
					break;
				}

				ParseHit( s, processHit );
			}
			return s;
		}

		private void ParseHit ( string s, Action<Hit> processHit ) {
			string[] t = s.Split( '\t' );

			try {
				int		fieldCounter	= 0;
				decimal queryId_		= decimal.Parse( t[fieldCounter++] );
				decimal hitId			= decimal.Parse( t[fieldCounter++] );
				double	distance		= double.Parse( t[fieldCounter++] );
				long	identity		= long.Parse( t[fieldCounter++] );
				long	positive		= long.Parse( t[fieldCounter++] );
				double	bitScore		= double.Parse( t[fieldCounter++] );
				double	eValue			= double.Parse( t[fieldCounter++] );
				long	hitFrom			= long.Parse( t[fieldCounter++] );
				long	hitTo			= long.Parse( t[fieldCounter++] );
				string	hitSequence		= t[fieldCounter++];
				long	hitLength		= long.Parse( t[fieldCounter++] );
				int		score			= int.Parse( t[fieldCounter++] );
				long	queryFrom		= long.Parse( t[fieldCounter++] );
				long	queryTo			= long.Parse( t[fieldCounter++] );
				long	queryLength		= long.Parse( t[fieldCounter++] );
				string	querySequence	= t[fieldCounter++];
				string	midline			= t[fieldCounter++];

				ILinearDomain querySeq = model.genes[queryId_];
				DefaultFeature queryFeature = (DefaultFeature) model.features.Add( new DefaultFeature( querySeq, queryFrom, queryTo, querySequence ) );

				ILinearDomain hitSeq = model.genes[hitId];
				DefaultFeature hitFeature = (DefaultFeature) model.features.Add( new DefaultFeature( hitSeq, hitFrom, hitTo, hitSequence ) );

				processHit( new Hit( queryFeature, hitFeature ) {
					EValue = eValue,
					Identities = identity,
					Midline = midline,
					Positives = positive
				} );
			}
			catch ( Exception e ) {
				throw new ParseError( "Error loading genes:\n{0}", e );
			}
		}

		private static string ParseRecords ( List<Dictionary<string, string>> records, StringReader r ) {
			string s = r.ReadLine();

			if ( s != "Begin Records" ) {
				throw new ParseError( "Error parsing genes - expected 'Begin Genes' but encountered '{0}'", s );
			}

			for ( ; ; ) {
				s = r.ReadLine();

				if ( s == null ) {
					throw new ParseError( "Unexpected end of input while parsing genes." );
				}

				if ( s == "End Records" ) {
					break;
				}

				ParseRecordAndAppend( s, records );
			}
			return s;
		}

		private static void ParseRecordAndAppend ( string s, List<Dictionary<string, string>> records ) {
			string[] parts = s.Split( '\t' );

			Dictionary<string, string> record = new Dictionary<string, string>();

			for ( int i = 0; i < parts.Length; i += 2 ) {
				record[parts[i]] = parts[i + 1];
			}

			records.Add( record );
		}

		private static bool AllOk ( string methodName, AsyncCompletedEventArgs args ) {
			if ( args.Cancelled ) {
				// Status.WriteLine( "{0}: cancelled.", methodName );
				return false;
			}
			else if ( args.Error != null ) {
				Alert.Show( "{0}: an error has occurred:\n{1}", methodName, args.Error );
				return false;
			}
			else {
				return true;
			}
		}

		public static string Unescape ( string s ) {
			return s.Replace( "&n;", "\n" ).Replace( "&t;", "\t" ).Replace( "&amp;", "&" );
		}
	}
}
